﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace Daybreak.Core.Network.Udp
{
    public class UdpNetServer : NetBase, IDisposable
    {
        private NetBase _this;

        private Socket server;

        private Queue<NetPacket> msgPool = new Queue<NetPacket>();

        private ManualResetEvent allDone = new ManualResetEvent(true);

        /// <summary>
        /// Hold all client connections
        /// </summary>
        private Dictionary<int, EndPoint> clientConnections = new Dictionary<int, EndPoint>();

        internal int count_connection_id = 0;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="assemblyFile">Path to assembly where are located user's packets</param>
        public UdpNetServer(string assemblyFile)
            : base(assemblyFile)
        {
            _this = this;

            OnError += new Error(UdpNetServer_OnError);

            this.CreatePacketConfig();
        }

        void UdpNetServer_OnError(string message)
        {
            Console.WriteLine(message);
        }

        public virtual void ClientCloseConnection(int id)
        {
            clientConnections.Remove(id);
        }

        public virtual void ClientOpenConnection(EndPoint ep, int id)
        {
            clientConnections.Add(id, ep);
        }

        /// <summary>
        /// Start listening for connections
        /// </summary>
        /// <param name="host">localhost or local ip</param>
        /// <param name="port">The local port number from which you intend to communicate</param>
        public void Start(string host, int port)
        {
            try
            {
                IPEndPoint iep = new IPEndPoint(Dns.GetHostEntry(host).AddressList[0], port);
                EndPoint ep = (EndPoint)iep;

                server = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
                server.Bind(ep);


                Thread listenThread = new Thread(new ParameterizedThreadStart(ThreadListen));
                listenThread.IsBackground = true;
                listenThread.Start(port);

                Thread sendThread = new Thread(new ThreadStart(ThreadSend));
                sendThread.IsBackground = true;
                sendThread.Start();
            }
            catch (SocketException ex)
            {
                NetError(ex.Message);
            }
        }


        /// <summary> 
        /// Send some data
        /// </summary>
        /// <param name="packet">User's packet</param>
        public override void Send(NetPacket packet)
        {
            msgPool.Enqueue(packet);

            base.Send(packet);
        }

        public void SendToAll(NetPacket packet)
        {
            foreach (EndPoint item in clientConnections.Values)
            {
                NetPacket pak = (NetPacket)Activator.CreateInstance(packet.GetType());
                pak.ClientID = packet.ClientID;
                pak.Data = packet.Data;
                pak.PacketID = packet.PacketID;
                pak.RemoteEndPoint = item;
                msgPool.Enqueue(pak);
            }
        }

        public int CountConnection
        {
            get
            {
                return clientConnections.Count;
            }
        }

        private void ThreadSend()
        {
            while (true)
            {
                if (msgPool.Count != 0)
                {
                    try
                    {
                        NetPacket packet = msgPool.Dequeue();

                        byte[] buffer = new byte[1];

                        int pid = GetPacketID(packet);
                        int lpid = pid.ToString().Length;
                        buffer[0] = Convert.ToByte(lpid);

                        Array.Resize<byte>(ref buffer, buffer.Length + lpid);

                        Array.Copy(Encoding.ASCII.GetBytes(pid.ToString()), 0, buffer, 1, lpid);

                        Array.Resize<byte>(ref buffer, buffer.Length + packet.Data.Length);

                        Array.Copy(packet.Data, 0, buffer, 1 + lpid, packet.Data.Length);

                        server.SendTo(buffer, packet.RemoteEndPoint);
                    }
                    catch (SocketException ex)
                    {
                        NetError(ex.Message);
                    }
                }

                Thread.Sleep(5);
            }
        }

        private void ThreadListen(object data)
        {
            while (true)
            {
                try
                {
                    allDone.Reset();

                    IPEndPoint remoteIpEndPoint = new IPEndPoint(IPAddress.Any, (int)data);
                    EndPoint tempRemoteEP = (EndPoint)remoteIpEndPoint;

                    StateObject so = new StateObject();

                    so.socket = server;

                    server.BeginReceiveFrom(so.buffer, 0, StateObject.buffer_size, SocketFlags.None, ref tempRemoteEP, new AsyncCallback(this.AsyncEndReceiveFrom), so);

                    allDone.WaitOne();
                }
                catch (SocketException ex)
                {
                    NetError(ex.Message);
                }
            }
        }

        private void AsyncEndReceiveFrom(IAsyncResult ar)
        {
            try
            {
                StateObject so = (StateObject)ar.AsyncState;

                allDone.Set();

                Socket socket = so.socket;

                IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
                EndPoint tempRemoteEP = (EndPoint)sender;

                int size = socket.EndReceiveFrom(ar, ref tempRemoteEP);

                byte[] tmp = new byte[size];
                Array.Copy(so.buffer, 0, tmp, 0, size);

                DeterminePacket(tmp, tempRemoteEP);
            }
            catch (SocketException ex)
            {
                NetError(ex.Message);
            }
        }

        /// <summary>
        /// Determine packet
        /// </summary>
        /// <param name="data">Received data</param>
        private void DeterminePacket(byte[] data, EndPoint ep)
        {
            int lpid = Convert.ToInt32(data[0]);
            byte[] temp0 = new byte[lpid];
            Array.Copy(data, 2, temp0, 0, lpid);

            int lid = Convert.ToInt32(data[1]);
            byte[] temp1 = new byte[lid];
            Array.Copy(data, 2 + lpid, temp1, 0, lid);

            NetPacket packet = GetPacket(int.Parse(Encoding.ASCII.GetString(temp0)));

            packet.Data = new byte[data.Length - 2 - lpid - lid];
            Array.Copy(data, 2 + lpid + lid, packet.Data, 0, data.Length - 2 - lpid - lid);

            packet.RemoteEndPoint = ep;
            packet.ClientID = int.Parse(Encoding.ASCII.GetString(temp1));
            packet.ServerAction(ref _this);
        }

        public void Dispose()
        {
            server.Close();
            server.Shutdown(SocketShutdown.Both);
        }
    }
}
